package org.jenkinsci.plugins.p4scm;

import static hudson.Util.fixNull;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;

import jenkins.model.Jenkins;
import net.sf.json.JSONObject;

import org.jenkinsci.plugins.p4scm.config.CleanTypeConfig;
import org.jenkinsci.plugins.p4scm.config.DepotType;
import org.jenkinsci.plugins.p4scm.config.MaskViewConfig;
import org.jenkinsci.plugins.p4scm.config.WorkspaceCleanupConfig;
import org.jenkinsci.plugins.p4scm.utils.CommonUtil;
import org.jenkinsci.plugins.p4scm.utils.MacroStringHelper;
import org.jenkinsci.plugins.p4scm.utils.ParameterSubstitutionException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.tek42.perforce.Depot;
import com.tek42.perforce.PerforceException;
import com.tek42.perforce.model.Changelist;
import com.tek42.perforce.model.Changelist.FileEntry;
import com.tek42.perforce.model.Counter;
import com.tek42.perforce.model.Label;
import com.tek42.perforce.model.Workspace;
import com.tek42.perforce.parse.Counters;

import hudson.AbortException;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.matrix.MatrixRun;
import hudson.matrix.MatrixBuild;
import hudson.model.BuildListener;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.Job;
import hudson.model.Node;
import hudson.model.Project;
import hudson.model.Run;
import hudson.model.listeners.ItemListener;
import hudson.scm.ChangeLogParser;
import hudson.scm.PollingResult;
import hudson.scm.RepositoryBrowser;
import hudson.scm.SCMDescriptor;
import hudson.scm.SCMRevisionState;
import hudson.scm.SCM;
import hudson.util.FormValidation;
import hudson.util.LogTaskListener;
import hudson.util.StreamTaskListener;

/**
 * @author yzhou-citrix
 *
 */
@SuppressWarnings("unused")
public class P4SCM extends SCM {
    
    private static final Logger logger = LoggerFactory.getLogger(P4SCM.class);
    private static final int MAX_CHANGESETS_ON_FIRST_BUILD = 50;
    private static final String WORKSPACE_COMBINATOR = System.getProperty(hudson.slaves.WorkspaceList.class.getName(),"@");
    private static final int MAX_BUILD_ENV_VARS_NESTED_CALLS = 2;
    
    private static final String[] P4CLIENT_FORBIDDEN_VARIABLES= 
            {"EXECUTOR_NUMBER"};
    
    private Long configVersion;
    
    
    /*
     * Depot related variables
     */
    String p4Tool;
    String p4Port;
    String p4User;
    String p4Passwd;
    /**
     * If true the environment value P4PASSWD will be set to the value of p4Passwd.
     */
    boolean exposeP4Passwd = false;
    
    /*
     * Project Details related variables
     * */
    String p4Client;
    /**
     * If true, we will create the workspace view within the plugin.  If false, we will not.
     */
    boolean createWorkspace = true;
    /**
     * If true, we will manage the workspace view within the plugin.  If false, we will leave the
     * view alone.
     */
    boolean updateView = true;
    /* Client view type*/
    String p4Stream;
    String clientSpec;
    String projectPath;
    /**
     * Use ClientSpec text file from depot to prepare the workspace view
     */
    boolean useClientSpec = false;
    /**
     * True if stream depot is used, false otherwise
     */
    boolean useStreamDepot = false;
    
    /**
     * If true, the workspace will be deleted before the checkout commences.
     */
    boolean wipeBeforeBuild = false;

    /**
     * If true, the workspace will be cleaned before the checkout commences.
     */
    boolean quickCleanBeforeBuild = false;
    
    /**
     * If true, files in the workspace will be scanned for differences and restored during a quick clean
     */
    boolean restoreChangedDeletedFiles = false;
    
    /**
     * If true, the ,repository will be deleted before the checkout commences in addition to the workspace.
     */
    boolean wipeRepoBeforeBuild = false;
    
    String projectOptions;
    String clientOwner;
    
    /**
     * force sync is a one time trigger from the config area to force a sync with the depot.
     * it is reset to false after the first checkout.
     */
    boolean forceSync = false;
    /**
     * Always force sync the workspace when running a build
     */
    boolean alwaysForceSync = false;
    
    /**
     * Disable ChangeLog retrieval
     */
    boolean disableChangeLogOnly = false;
    /**
     * Disable Workspace syncing
     */
    boolean disableSyncOnly = false;
    
    /**
     * Show integrated changelists
     */
    boolean showIntegChanges = false;
    
    /**
     * Sync only on master option.
     */
    private boolean pollOnlyOnMaster = false;
    
    /**
     * If > 0, then will override the changelist we sync to for the first build.
     */
    int firstChange = -1;
    /**
     * Maximum amount of files that are recorded to a changelist, if < 1 show every file.
     */
    int fileLimit = 0;
    
    /**
     * The value of the LineEnd field in the perforce Client spec.
     */
    private String lineEndValue = "local";
    
    String p4Label;
    String p4Counter;
    /**
     * If true we update the named counter to the last changelist value after the sync operation.
     * If false the counter will be used as the changelist value to sync to.
     * Defaulting to false since the counter name is not set to begin with.
     */
    boolean updateCounterValue = false;
    String p4UpstreamProject;
    
    /**
     * If true, we will never update the client workspace spec on the perforce server.
     */
    boolean dontUpdateClient = false;
    /**
     * Don't update the 'have' database on the server when syncing.
     */
    boolean dontUpdateServer = false;
    
    /**
     * Determines what to append to the end of the client workspace names on slaves
     * Possible values:
     *  None
     *  Hostname
     *  Hash
     */
    String slaveClientNameFormat = null;
    
    String p4SysDrive = "C:";
    String p4SysRoot = "C:\\WINDOWS";
    
    /**
     * charset options
     */
    private String p4Charset = null;
    private String p4CommandCharset = null;
    
    
    /**
     * P4 user name(s) or regex user pattern to exclude from SCM poll to prevent build trigger.
     * Multiple user names are deliminated by space.
     */
    String excludedUsers;

    /**
     * P4 file(s) or regex file pattern to exclude from SCM poll to prevent build trigger.
     */
    String excludedFiles;

    /**
     * Use Case sensitive matching on excludedFiles.
     */
    boolean excludedFilesCaseSensitivity;
    
    /**
     * View mask settings for polling and/or syncing against a subset
     * of files in the client workspace.
     */
    private boolean useViewMask = false;
    private String viewMask = null;
    private boolean useViewMaskForPolling = true;
    private boolean useViewMaskForSyncing = false;
    private boolean useViewMaskForChangeLog = false;

    /**
     * If a ticket was issued we can use it instead of the password in the environment.
     */
    private String p4Ticket = null;
    
    P4RepositoryBrowser browser;
    
    public String getP4User() {
        return p4User;
    }

    public void setP4User(String p4User) {
        this.p4User = p4User;
    }

    public String getP4Passwd() {
        return p4Passwd;
    }
    
    public String getDecryptedP4Passwd() {
        P4PasswordEncryptor encryptor = P4PasswordEncryptor.instance();
        return encryptor.decryptString(getEffectiveP4Password());
    }
    
    public String getDecryptedP4Passwd(AbstractBuild<?,?> build) 
            throws ParameterSubstitutionException, InterruptedException {
        return MacroStringHelper.substituteParameters(getDecryptedP4Passwd(), this, build, null);
    }

    
    public String getDecryptedP4Passwd(@CheckForNull AbstractProject<?,?> project, @CheckForNull Node node) 
            throws ParameterSubstitutionException, InterruptedException {
        return MacroStringHelper.substituteParameters(getDecryptedP4Passwd(),
                this, project, node, null);
    }

    public void setP4Passwd(String p4Passwd) {
        
        P4PasswordEncryptor encryptor = P4PasswordEncryptor.instance();
        
        if(encryptor.appearsToBeAnEncryptedPassword(p4Passwd)) {
            this.p4Passwd = p4Passwd;
        } else {
            this.p4Passwd = encryptor.encryptString(p4Passwd);
        }
        
    }

    public String getP4Port() {
        return p4Port;
    }

    public void setP4Port(String p4Port) {
        this.p4Port = p4Port;
    }

    public String getP4Tool() {
        return p4Tool;
    }

    public void setP4Tool(String p4Tool) {
        this.p4Tool = p4Tool;
    }

    public boolean isExposeP4Passwd() {
        return exposeP4Passwd;
    }

    public void setExposeP4Passwd(boolean exposeP4Passwd) {
        this.exposeP4Passwd = getDescriptor().isPasswordExposeDisabled()? false : exposeP4Passwd;
    }
    
    /**
     * Check if we are using a stream depot type or a classic depot type.
     *
     * @return True if we are using a stream depot type, False otherwise
     */
    public boolean isUseStreamDepot() {
        return useStreamDepot;
    }

    /**
     * Control the usage of stream depot.
     *
     * @param useStreamDepot True if stream depot is used, False otherwise
     */
    public void setUseStreamDepot(boolean useStreamDepot) {
        this.useStreamDepot = useStreamDepot;
    }

    /**
     * Get the stream name.
     *
     * @return the p4Stream
     */
    public String getP4Stream() {
        return p4Stream;
    }

    /**
     * Set the stream name.
     *
     * @param stream the stream name
     */
    public void setP4Stream(String stream) {
        p4Stream = stream;
    }
    
    /**
     * @return the p4Client
     */
    public String getP4Client() {
        return p4Client;
    }

    /**
     * @param client the p4Client to set
     */
    public void setP4Client(String client) {
        p4Client = client;
    }
    
    /**
     * @return the path to the ClientSpec
     */
    public String getClientSpec() {
        return clientSpec;
    }

    /**
     * @param clientSpec the path to the ClientSpec
     */
    public void setClientSpec(String clientSpec) {
        this.clientSpec = clientSpec;
    }

    /**
     * @return True if we are using a ClientSpec file to setup the workspace view
     */
    public boolean isUseClientSpec() {
        return useClientSpec;
    }

    /**
     * @param useClientSpec True if a ClientSpec file should be used to setup workspace view, False otherwise
     */
    public void setUseClientSpec(boolean useClientSpec) {
        this.useClientSpec = useClientSpec;
    }
    
    
    /**
     * @return the projectPath
     */
    public String getProjectPath() {
        return projectPath;
    }

    /**
     * @param projectPath the projectPath to set
     */
    public void setProjectPath(String projectPath) {
        // Make it backwards compatible with the old way of specifying a label
        Matcher m = Pattern.compile("(@\\S+)\\s*").matcher(projectPath);
        if (m.find()) {
            p4Label = m.group(1);
            projectPath = projectPath.substring(0,m.start(1))
                + projectPath.substring(m.end(1));
        }
        this.projectPath = projectPath;
    }
    
    /**
     * The current perforce option set for the view.
     * @return current perforce view options
     */
    public String getProjectOptions() {
        return projectOptions;
    }

    /**
     * Set the perforce options for view creation.
     * @param projectOptions the effective perforce options.
     */
    public void setProjectOptions(String projectOptions) {
        this.projectOptions = projectOptions;
    }
    
    /**
     * @return  True if we are performing a one-time force sync
     */
    public boolean isForceSync() {
        return this.forceSync;
    }

    /**
     * @return  True if we are performing a one-time force sync
     */
    public boolean isAlwaysForceSync() {
        return this.alwaysForceSync;
    }
    
    /**
     * @param force True to perform a one time force sync, false to perform normal sync
     */
    public void setForceSync(boolean force) {
        this.forceSync = force;
    }

    /**
     * @param force True to perform a one time force sync, false to perform normal sync
     */
    public void setAlwaysForceSync(boolean force) {
        this.alwaysForceSync = force;
    }
       
    public boolean isDisableChangeLogOnly() {
        return disableChangeLogOnly;
    }

    public void setDisableChangeLogOnly(boolean disableChangeLogOnly) {
        this.disableChangeLogOnly = disableChangeLogOnly;
    }

    public boolean isDisableSyncOnly() {
        return disableSyncOnly;
    }

    public void setDisableSyncOnly(boolean disableSyncOnly) {
        this.disableSyncOnly = disableSyncOnly;
    }

    public String getClientOwner() {
        return clientOwner;
    }

    public void setClientOwner(String clientOwner) {
        this.clientOwner = clientOwner;
    }
    
    public boolean isShowIntegChanges() {
        return showIntegChanges;
    }

    public void setShowIntegChanges(boolean showIntegChanges) {
        this.showIntegChanges = showIntegChanges;
    }
    
    public boolean isPollOnlyOnMaster() {
        return pollOnlyOnMaster;
    }

    public void setPollOnlyOnMaster(boolean pollOnlyOnMaster) {
        this.pollOnlyOnMaster = pollOnlyOnMaster;
    }
    
    /**
     * This is only for the config screen.  Also, it returns a string and not an int.
     * This is because we want to show an empty value in the config option if it is not being
     * used.  The default value of -1 is not exactly empty.  So if we are set to default of
     * -1, we return an empty string.  Anything else and we return the actual change number.
     *
     * @return  The one time use variable, firstChange.
     */
    public String getFirstChange() {
        if (firstChange <= 0)
            return "";
        return Integer.valueOf(firstChange).toString();
    }

    public void setFirstChange(int firstChange) {
        this.firstChange = firstChange;
    }

    /**
     * This is only for the config screen.  Also, it returns a string and not an int.
     * This is because we want to show an empty value in the config option if it is not being
     * used.  The default value of -1 is not exactly empty.  So if we are set to default of
     * -1, we return an empty string.  Anything else and we return the actual change number.
     *
     * @return  fileLimit
     */
    public String getFileLimit() {
        if (fileLimit <= 0)
            return "";
        return Integer.valueOf(fileLimit).toString();
    }

    public void setFileLimit(int fileLimit) {
        this.fileLimit = fileLimit;
    }
    
    public String getLineEndValue() {
        return lineEndValue;
    }

    public void setLineEndValue(String lineEndValue) {
        this.lineEndValue = lineEndValue;
    }
    
    /**
     * @return the p4Label
     */
    public String getP4Label() {
        return p4Label;
    }
    
    /**
     * @return  True if we are using a label
     */
    public boolean isUseLabel() {
        return p4Label != null;
    }

    /**
     * @param label the p4Label to set
     */
    public void setP4Label(String label) {
        p4Label = label;
    }

    /**
     * @return the p4Counter
     */
    public String getP4Counter() {
        return p4Counter;
    }

    /**
     * @param counter the p4Counter to set
     */
    public void setP4Counter(String counter) {
        p4Counter = counter;
    }
    
    /**
     * @return True if the plugin should update the counter to the last change
     */
    public boolean isUpdateCounterValue() {
        return updateCounterValue;
    }

    /**
     * @param updateCounterValue True if the plugin should update the counter to the last change
     */
    public void setUpdateCounterValue(boolean updateCounterValue) {
        this.updateCounterValue = updateCounterValue;
    }

    /**
     * @return the p4UpstreamProject
     */
    public String getP4UpstreamProject() {
        return p4UpstreamProject;
    }
    
    /**
     * @param project the p4UpstreamProject to set
     */
    public void setP4UpstreamProject(String project) {
        p4UpstreamProject = project;
    }
    
    public boolean isDontUpdateClient() {
        return dontUpdateClient;
    }

    public void setDontUpdateClient(boolean dontUpdateClient) {
        this.dontUpdateClient = dontUpdateClient;
    }
    
    public boolean isDontUpdateServer() {
        return dontUpdateServer;
    }

    public void setDontUpdateServer(boolean dontUpdateServer) {
        this.dontUpdateServer = dontUpdateServer;
    }
    
    /**
     * Gets the client format for remote nodes.
     * @return Client format for remote nodes.
     *      Null and empty values will be replaced by a default format
     */
    public @Nonnull String getSlaveClientNameFormat() {   
        @CheckForNull String effectiveSlaveClientNameFormat = Util.fixEmpty(this.slaveClientNameFormat);
        if (effectiveSlaveClientNameFormat != null) {
            return effectiveSlaveClientNameFormat;
        }
        
        //Hash should be the new default
        effectiveSlaveClientNameFormat = "${basename}-${hash}";
        return effectiveSlaveClientNameFormat;
    }
    
    /**
     * @param clientFormat A string defining the format of the client name for slave workspaces.
     */
    public void setSlaveClientNameFormat(String clientFormat) {
        this.slaveClientNameFormat = clientFormat;
    }
    
    /**
     * @return the p4SysDrive
     */
    public String getP4SysDrive() {
        return p4SysDrive;
    }

    /**
     * @param sysDrive the p4SysDrive to set
     */
    public void setP4SysDrive(String sysDrive) {
        p4SysDrive = sysDrive;
    }

    /**
     * @return the p4SysRoot
     */
    public String getP4SysRoot() {
        return p4SysRoot;
    }

    /**
     * @param sysRoot the p4SysRoot to set
     */
    public void setP4SysRoot(String sysRoot) {
        p4SysRoot = sysRoot;
    }
    
    public String getP4Charset() {
        return p4Charset;
    }

    public void setP4Charset(String p4Charset) {
        this.p4Charset = p4Charset;
    }

    public String getP4CommandCharset() {
        return p4CommandCharset;
    }

    public void setP4CommandCharset(String p4CommandCharset) {
        this.p4CommandCharset = p4CommandCharset;
    }
    
    public String getExcludedUsers() {
        return excludedUsers;
    }

    public void setExcludedUsers(String users) {
        excludedUsers = users;
    }
    
    public String getExcludedFiles() {
        return excludedFiles;
    }

    public void setExcludedFiles(String files) {
        excludedFiles = files;
    }
    
    public boolean getExcludedFilesCaseSensitivity() {
        return excludedFilesCaseSensitivity;
    }

    public void setExcludedFilesCaseSensitivity(boolean excludedFilesCaseSensitivity) {
        this.excludedFilesCaseSensitivity = excludedFilesCaseSensitivity;
    }
    
    public boolean isUseViewMaskForPolling() {
        return useViewMaskForPolling;
    }

    public void setUseViewMaskForPolling(boolean useViewMaskForPolling) {
        this.useViewMaskForPolling = useViewMaskForPolling;
    }

    public boolean isUseViewMaskForSyncing() {
        return useViewMaskForSyncing;
    }

    public void setUseViewMaskForSyncing(boolean useViewMaskForSyncing) {
        this.useViewMaskForSyncing = useViewMaskForSyncing;
    }

    public boolean isUseViewMaskForChangeLog() {
        return useViewMaskForChangeLog;
    }

    public void setUseViewMaskForChangeLog(boolean useViewMaskForChangeLog) {
        this.useViewMaskForChangeLog = useViewMaskForChangeLog;
    }
    
    public String getViewMask() {
        return viewMask;
    }

    public void setViewMask(String viewMask) {
        this.viewMask = viewMask;
    }

    public boolean isUseViewMask() {
        return useViewMask;
    }

    public void setUseViewMask(boolean useViewMask) {
        this.useViewMask = useViewMask;
    }
    
    
    @Override
    public P4RepositoryBrowser getBrowser() {
        logger.debug("Get Browser");
        return browser;
    }

    @DataBoundConstructor
    public P4SCM(
            String p4Tool,
            String p4Port,
            String p4User,
            String p4Passwd,
            boolean exposeP4Passwd,
            
            String p4Client,
            boolean createWorkspace,
            boolean updateView,
            DepotType depotType,
            
            WorkspaceCleanupConfig cleanWorkspace,
            String projectOptions,
            String clientOwner,
            boolean forceSync,
            boolean alwaysForceSync,
            boolean disableChangeLogOnly,
            boolean disableSyncOnly,
            boolean showIntegChanges,
            boolean pollOnlyOnMaster,
            int firstChange,
            int fileLimit,
            String lineEndValue,
            String p4Label,
            String p4Counter,
            boolean updateCounterValue,
            String p4UpstreamProject,
            boolean dontUpdateClient,
            boolean dontUpdateServer,
            String slaveClientNameFormat,
            String p4SysRoot,
            String p4SysDrive,
            String p4Charset,
            String p4CommandCharset,
            String excludedUsers,
            String excludedFiles,
            boolean excludedFilesCaseSensitivity,
            MaskViewConfig useViewMask,
            P4RepositoryBrowser browser
            ) {
        logger.debug("Enter P4SCM Constructor");
        
        this.configVersion = 2L;
        
        this.p4Tool = p4Tool;
        this.p4Port = p4Port;
        this.p4User = Util.fixEmptyAndTrim(p4User);
        this.setP4Passwd(p4Passwd);//this.p4Passwd = p4Passwd;
        this.setExposeP4Passwd(exposeP4Passwd);//this.exposeP4Passwd = exposeP4Passwd;
        
        this.p4Client = p4Client;
        this.createWorkspace = createWorkspace;
        this.updateView = updateView;
        
        // Get data from the depot type
        if (depotType != null) {
            this.p4Stream = depotType.getP4Stream();
            this.clientSpec = depotType.getClientSpec();
            this.projectPath = Util.fixEmptyAndTrim(depotType.getProjectPath());
            this.useStreamDepot = depotType.useP4Stream();
            this.useClientSpec = depotType.useClientSpec();
            this.useViewMask = depotType.useProjectPath();
        }
        
        // Get data from workspace cleanup settings
        if (cleanWorkspace != null) {          
            setWipeRepoBeforeBuild(cleanWorkspace.isWipeRepoBeforeBuild());
            
            CleanTypeConfig cleanType = cleanWorkspace.getCleanType();
            if (cleanType != null) {
                setWipeBeforeBuild(cleanType.isWipe());
                setQuickCleanBeforeBuild(cleanType.isQuick());
                setRestoreChangedDeletedFiles(cleanType.isRestoreChangedDeletedFiles());
            }          
        } else {
            setWipeRepoBeforeBuild(false);
        }
        
        // Set the workspace options according to the configuration
        this.projectOptions = (projectOptions != null)
                ? projectOptions
                : "noallwrite clobber nocompress unlocked nomodtime rmdir";
        
        this.clientOwner = Util.fixEmptyAndTrim(clientOwner);
        this.forceSync = forceSync;
        this.alwaysForceSync = alwaysForceSync;
        this.disableChangeLogOnly = disableChangeLogOnly;
        this.disableSyncOnly = disableSyncOnly;
        this.showIntegChanges = showIntegChanges;
        this.pollOnlyOnMaster = pollOnlyOnMaster;
        this.firstChange = firstChange;
        this.fileLimit = fileLimit;
        this.lineEndValue = lineEndValue;
        if (this.p4Label != null && p4Label != null) { // TODO: what's the usage?
            logger.warn("Label found in views and in label field.  Using: " + p4Label);
        }
        this.p4Label = Util.fixEmptyAndTrim(p4Label);

        this.p4Counter = Util.fixEmptyAndTrim(p4Counter);
        this.updateCounterValue = updateCounterValue;
        this.p4UpstreamProject = Util.fixEmptyAndTrim(p4UpstreamProject);
        this.dontUpdateClient = dontUpdateClient;
        this.dontUpdateServer = dontUpdateServer;
        this.slaveClientNameFormat = slaveClientNameFormat;
        
        if (p4SysRoot != null) {
            this.p4SysRoot = p4SysRoot.trim();
        }
        if (p4SysDrive != null) {
            this.p4SysDrive = p4SysDrive.trim();
        }
        
        this.p4Charset = Util.fixEmptyAndTrim(p4Charset);
        this.p4CommandCharset = Util.fixEmptyAndTrim(p4CommandCharset);
        
        this.excludedUsers = Util.fixEmptyAndTrim(excludedUsers);
        this.excludedFiles = Util.fixEmptyAndTrim(excludedFiles);
        this.excludedFilesCaseSensitivity = excludedFilesCaseSensitivity;
        
        // Setup view mask
        if (useViewMask != null) {
            setUseViewMask(true);
            setViewMask(hudson.Util.fixEmptyAndTrim(useViewMask.getViewMask()));
            setUseViewMaskForPolling(useViewMask.isUseViewMaskForPolling());
            setUseViewMaskForSyncing(useViewMask.isUseViewMaskForSyncing());         
            setUseViewMaskForChangeLog(useViewMask.isUseViewMaskForChangeLog());
            
        } else {
            setUseViewMask(false);
        }
        
        this.browser = browser;
        logger.debug("p4User: {}, p4Port:{}, p4Passwd:{}, p4Client: {}\n, depotType: {}", 
                this.p4User, this.p4Port, this.p4Passwd, this.p4Client, depotType);
    }
    
    
    /**
     * @return True if the plugin is to delete the workpsace files before building.
     */
    public boolean isWipeBeforeBuild() {
        return wipeBeforeBuild;
    }
    
    /**
     * @param wipeBeforeBuild True if the client is to delete the workspace files before building.
     */
    public void setWipeBeforeBuild(boolean wipeBeforeBuild) {
        this.wipeBeforeBuild = wipeBeforeBuild;
    }
    
    public boolean isQuickCleanBeforeBuild() {
        return quickCleanBeforeBuild;
    }
    
    public void setQuickCleanBeforeBuild(boolean quickCleanBeforeBuild) {
        this.quickCleanBeforeBuild = quickCleanBeforeBuild;
    }

    /**
     * @return True if the plugin is to clean the workspace using any method before building.
     */
    public boolean isCleanWorkspaceBeforeBuild() {
        return wipeBeforeBuild || quickCleanBeforeBuild;
    }
    
    public boolean isRestoreChangedDeletedFiles() {
        return restoreChangedDeletedFiles;
    }

    public void setRestoreChangedDeletedFiles(boolean restoreChangedDeletedFiles) {
        this.restoreChangedDeletedFiles = restoreChangedDeletedFiles;
    }
    
    /**
     * @return True if the plugin is to delete the workpsace including the.repository files before building.
     */
    public boolean isWipeRepoBeforeBuild() {
        return wipeRepoBeforeBuild;
    }
    
    public void setWipeRepoBeforeBuild(boolean wipeRepoBeforeBuild) {
        this.wipeRepoBeforeBuild = wipeRepoBeforeBuild;
    }
    
    /**
     * We need to store the changelog file name for the build so that we can expose
     * it to the build environment
     */
    transient private String changelogFilename = null;
    
    private static boolean overrideWithBooleanParameter(String paramName, 
            @Nonnull AbstractBuild<?,?> build, boolean dflt) {
        if (build.getBuildVariables() != null) {
            Object param = build.getBuildVariables().get(paramName);
            if (param != null) {
                String paramString = param.toString();
                return paramString.toUpperCase().equals("TRUE") || paramString.equals("1");
            }
        }
        
        return dflt;
    }
    
    public List<String> getAllLineEndChoices() {
        List<String> allChoices = this.getDescriptor().getAllLineEndChoices();
        ArrayList<String> choices = new ArrayList<String>();
        // Order choices so that the current one is first in the list
        // This is required in order for tests to work, unfortunately
        choices.add(lineEndValue);
        for (String choice : allChoices) {
            if (!choice.equals(lineEndValue)) {
                choices.add(choice);
            }
        }
        return choices;
    }

    /*
    @Override
    public void checkout(Run<?, ?> build, Launcher launcher,
            FilePath workspace, TaskListener listener, File changelogFile,
            SCMRevisionState baseline) throws IOException, InterruptedException {
        
        logger.info("checkout");
        
        PrintStream log = listener.getLogger();
        
        changelogFilename = changelogFile.getAbsolutePath();
        log.println(String.format("The change log file name: %s", changelogFilename));
        
        // HACK: Force build env vars to initialize
        // MacroStringHelper.substituteParameters("", this, build, null);
        
        if(build instanceof AbstractBuild) {
            log.println("Build is AbstractBuild");
        }
        
        //super.checkout(build, launcher, workspace, listener, changelogFile, baseline);
    }*/
    
    /** Get effective project path base on user's setting on UI
     * @param build
     * @param project
     * @param node
     * @param log
     * @param depot
     * @return
     * @throws PerforceException
     * @throws ParameterSubstitutionException
     * @throws InterruptedException
     */
    @Nonnull
    private String getEffectiveProjectPath(
            @CheckForNull AbstractBuild<?,?> build, 
            @Nonnull AbstractProject<?,?> project, 
            @CheckForNull Node node,
            @Nonnull PrintStream log, 
            @Nonnull Depot depot) 
            throws PerforceException, ParameterSubstitutionException, InterruptedException {
        String effectiveProjectPath = useClientSpec 
                ? getEffectiveProjectPathFromFile(build, project, node, log, depot)
                : MacroStringHelper.substituteParameters(this.projectPath, this, build, project, node, null);
        return effectiveProjectPath;
    }
    
    @Nonnull
    private String getEffectiveProjectPathFromFile(
            @CheckForNull AbstractBuild<?,?> build, 
            @CheckForNull AbstractProject<?,?> project, 
            @CheckForNull Node node, 
            @Nonnull PrintStream log, 
            @Nonnull Depot depot) 
            throws PerforceException, ParameterSubstitutionException, InterruptedException {
        String effectiveClientSpec = 
                MacroStringHelper.substituteParameters(this.clientSpec, this, build, project, node, null);
        
        log.println("Read ClientSpec from: " + effectiveClientSpec);
        com.tek42.perforce.parse.File f = depot.getFile(effectiveClientSpec);
        String effectiveProjectPath = 
                MacroStringHelper.substituteParameters(f.read(), this, build, project, node, null);
       
        return effectiveProjectPath;
    }
    
    private boolean nodeIsRemote(@CheckForNull Node buildNode) {
        return buildNode != null && buildNode.getNodeName().length() != 0;
    }
    
    private String getEffectiveClientName(@Nonnull AbstractBuild build, @CheckForNull Map<String,String> env) 
            throws ParameterSubstitutionException, InterruptedException {   
        final Node buildNode = build.getBuiltOn();
        final Map<String, String> extraVars = new TreeMap<String,String>();
        if (env != null) {
            extraVars.putAll(env);
        }

        String effectiveP4Client;
        if (buildNode!=null && nodeIsRemote(buildNode)) {
            final String effectiveSlaveClientNameFormat = getSlaveClientNameFormat();
            extraVars.put("basename", MacroStringHelper.substituteParameters(this.p4Client, this, build, extraVars));
            effectiveP4Client = MacroStringHelper.substituteParameters(effectiveSlaveClientNameFormat, this, build, extraVars);
        } else { // don't use node formats on master
            effectiveP4Client = MacroStringHelper.substituteParameters(this.p4Client, this, build, extraVars);
        }

        // eliminate spaces, just in case
        effectiveP4Client = effectiveP4Client.replaceAll(" ", "_");
        return effectiveP4Client;
    }

    private String getEffectiveClientName(@CheckForNull AbstractProject<?,?> project, @CheckForNull Node buildNode)
            throws IOException, InterruptedException {
              
        String effectiveP4Client;
        if (buildNode!=null && nodeIsRemote(buildNode)) {       
            final String effectiveRemoteClientNameFormat = getSlaveClientNameFormat();
            Map<String, String> extraVars = new TreeMap<String,String>();
            extraVars.put("basename", MacroStringHelper.substituteParameters(this.p4Client, this, project, buildNode, null));           
            effectiveP4Client = MacroStringHelper.substituteParameters (effectiveRemoteClientNameFormat, this, project, buildNode, extraVars);
        } else {
            effectiveP4Client = MacroStringHelper.substituteParameters(this.p4Client, this, project, buildNode, null);
        }
            
        // eliminate spaces, just in case
        effectiveP4Client = effectiveP4Client.replaceAll(" ", "_");
        return effectiveP4Client;
    }
    
    /**
     * Append Perforce workspace name with a Jenkins workspace identifier, if this
     * is a concurrent build job.
     *
     * @param workspace Workspace of the current build
     * @param p4Client User defined client name
     * @return The new client name. If this is a concurrent build with, append the
     * client name with a Jenkins workspace identifier.
     */
    private String getConcurrentClientName(FilePath workspace, String p4Client) {
        if (workspace != null) {
            // Match @ followed by an integer at the end of the workspace path
            Pattern p = Pattern.compile(".*" + Pattern.quote(WORKSPACE_COMBINATOR) + "(\\d+)$");
            Matcher matcher = p.matcher(workspace.getRemote());
            if (matcher.find()) {
                p4Client += "_" + matcher.group(1);
            }
        }
        return p4Client;
    }
    
    private Workspace getPerforceWorkspace(AbstractProject<?,?> project, String projectPath,
            Depot depot, Node buildNode, AbstractBuild<?,?> build,
            Launcher launcher, FilePath workspace, TaskListener listener, boolean dontChangeRoot)
        throws IOException, InterruptedException, PerforceException
    {
        return getPerforceWorkspace(project, projectPath, depot, buildNode, build, launcher, workspace, listener, dontChangeRoot, updateView);
    }

    private Workspace getPerforceWorkspace(AbstractProject<?,?> project, String projectPath,
            Depot depot, Node buildNode, AbstractBuild<?,?> build,
            Launcher launcher, FilePath workspace, TaskListener listener, boolean dontChangeRoot, boolean updateView) 
                    throws IOException, InterruptedException, PerforceException {
        PrintStream log = listener.getLogger();

        // If we are building on a slave node, and each node is supposed to have
        // its own unique client, then adjust the client name accordingly.
        // make sure each slave has a unique client name by adding it's
        // hostname to the end of the client spec

        String effectiveP4Client = build != null
                ? getEffectiveClientName(build, null)
                : getEffectiveClientName(project, buildNode);

        // If we are running concurrent builds, the Jenkins workspace path is different
        // for each concurrent build. Append Perforce workspace name with Jenkins
        // workspace identifier suffix. But, only do this if we are syncing or allowing
        // Jenkins to manage workspaces. //TODO: how to check if running concurrent builds
        if(!this.disableSyncOnly || this.createWorkspace) {
            effectiveP4Client = getConcurrentClientName(workspace, effectiveP4Client);
        }        

        if (!nodeIsRemote(buildNode)) {
            log.print("Using master perforce client: ");
            log.println(effectiveP4Client);
        }
        else {
            log.println("Using remote perforce client: " + effectiveP4Client);
        }

        depot.setClient(effectiveP4Client);
        String effectiveP4Stream = MacroStringHelper.substituteParameters(this.p4Stream, this, build, project, buildNode, null);

        // Get the clientspec (workspace) from perforce
        Workspace p4workspace = depot.getWorkspaces().getWorkspace(effectiveP4Client, effectiveP4Stream);
        assert p4workspace != null;
        boolean creatingNewWorkspace = p4workspace.isNew();

        // If the client workspace doesn't exist, and we're not managing the clients,
        // Then terminate the build with an error
        if (!createWorkspace && creatingNewWorkspace) {
            log.println("*** Perforce client workspace '" + effectiveP4Client +"' doesn't exist.");
            log.println("*** Please create it, or allow Jenkins to manage clients on it's own.");
            log.println("*** If the client name mentioned above is not what you expected, ");
            log.println("*** check your 'Client name format for slaves' advanced config option.");
            throw new AbortException("Error accessing perforce workspace.");
        }

        // Ensure that the clientspec (workspace) name is set correctly
        // TODO Examine why this would be necessary.

        p4workspace.setName(effectiveP4Client);

        // Set the workspace options according to the configuration
        if (projectOptions != null)
            p4workspace.setOptions(projectOptions);

        // Set the line ending option according to the configuration
        if (lineEndValue != null && getAllLineEndChoices().contains(lineEndValue)) {
            p4workspace.setLineEnd(lineEndValue);
        }

        if (clientOwner != null && !clientOwner.trim().isEmpty()) {
            p4workspace.setOwner(clientOwner);
        }

        // Ensure that the root is appropriate (it might be wrong if the user
        // created it, or if we previously built on another node).

        // Both launcher and workspace can be null if requiresWorkspaceForPolling returns true
        // So provide 'reasonable' default values.
        boolean isunix = true;
        if (launcher != null)
            isunix = launcher.isUnix();

        String localPath = CommonUtil.unescapeP4String(p4workspace.getRoot());

        if (workspace != null)
            localPath = CommonUtil.getLocalPathName(workspace, isunix);
        else if (localPath.trim().equals(""))
            localPath = project.getRootDir().getAbsolutePath();
        localPath = CommonUtil.escapeP4String(localPath);

        if (!localPath.equals(p4workspace.getRoot()) && !dontChangeRoot && !dontUpdateClient) {
            log.println("Changing P4 Client Root to: " + localPath);
            forceSync = true;
            p4workspace.setRoot(localPath);
        }

        if (updateView || creatingNewWorkspace) {
            // Switch to another stream view if necessary
            if (useStreamDepot) {
                p4workspace.setStream(effectiveP4Stream);
            }
            // If necessary, rewrite the views field in the clientspec. Also, clear the stream.
            // TODO If dontRenameClient==false, and updateView==false, user
            // has a lot of work to do to maintain the clientspecs.  Seems like
            // we could copy from a master clientspec to the slaves.
            else {
                p4workspace.setStream("");
                if (useClientSpec) {
                    projectPath = getEffectiveProjectPathFromFile(build, project, buildNode, log, depot);
                }
                List<String> mappingPairs = CommonUtil.parseProjectPath(projectPath, effectiveP4Client, log);
                if (!CommonUtil.equalsProjectPath(mappingPairs, p4workspace.getViews())) {
                    log.println("Changing P4 Client View from:\n" + p4workspace.getViewsAsString());
                    log.println("Changing P4 Client View to: ");
                    p4workspace.clearViews();
                    for (int i = 0; i < mappingPairs.size(); ) {
                        String depotPath = mappingPairs.get(i++);
                        String clientPath = mappingPairs.get(i++);
                        p4workspace.addView(" " + depotPath + " " + clientPath);
                        log.println("  " + depotPath + " " + clientPath);
                    }
                }
            }
        }
        // Clean host field so the client can be used on other slaves
        // such as those operating with the workspace on a network share
        p4workspace.setHost("");

        // NOTE: The workspace is not saved.
        return p4workspace;
    }
    
    private void saveWorkspaceIfDirty(Depot depot, Workspace p4workspace, PrintStream log) throws PerforceException {
        if (dontUpdateClient) {
            log.println("'Don't update client' is set. Not saving the client changes.");
            return;
        }
        if (p4workspace.isNew()) {
            log.println("Saving new client " + p4workspace.getName());
            depot.getWorkspaces().saveWorkspace(p4workspace);
        } else if (p4workspace.isDirty()) {
            log.println("Saving modified client " + p4workspace.getName());
            depot.getWorkspaces().saveWorkspace(p4workspace);
        }
    }
    
    public int getLastChange(@CheckForNull Run<?,?> build) {
        // If we are starting a new hudson project on existing work and want to skip the prior history...
        if (firstChange > 0)
            return firstChange;

        return CommonUtil.getLastChangeNoFirstChange(build);
    }
    
    /**
     * Determines whether or not P4 changelist should be excluded and ignored by the polling trigger.
     * Exclusions include files, regex patterns of files, and/or changelists submitted by a specific user(s).
     *
     * @param changelist the p4 changelist
     * @return  True if changelist only contains user(s) and/or file(s) that are denoted to be excluded
     */
    private boolean isChangelistExcluded(Changelist changelist, 
            AbstractProject<?,?> project, Node node, String view, PrintStream logger) 
            throws ParameterSubstitutionException, InterruptedException
    {
        if (changelist == null) {
            return false;
        }

        if (excludedUsers != null && !excludedUsers.trim().equals("")) {
            List<String> users = Arrays.asList(
                    MacroStringHelper.substituteParameters(excludedUsers,this, project, node, null).split("\n"));

            if (users.contains(changelist.getUser())) {
                logger.println("Excluded User [" + changelist.getUser() + "] found in changelist.");
                return true;
            }

            // no literal match, try regex
            for (String regex : users) {
                try {
                    Matcher matcher = Pattern.compile(regex).matcher(changelist.getUser());
                    if (matcher.find()) {
                        logger.println("Excluded User ["+changelist.getUser()+"] found in changelist.");
                        return true;
                    }
                } catch (PatternSyntaxException pse) {
                    break;  // should never occur since we validate regex input before hand, but just be safe
                }
            }
        }

        if (excludedFiles != null && !excludedFiles.trim().equals("")) {
            List<String> files = Arrays.asList(
                    MacroStringHelper.substituteParameters(excludedFiles, this, project, node, null).split("\n"));
            
            if (files.size() > 0 && changelist.getFiles().size() > 0) {
                StringBuilder buff = new StringBuilder("Exclude file(s) found:\n");
                for (FileEntry f : changelist.getFiles()) {
                    if (!CommonUtil.doesFilenameMatchAnyP4Pattern(f.getFilename(),files,excludedFilesCaseSensitivity) &&
                            CommonUtil.isFileInView(f.getFilename(), view, excludedFilesCaseSensitivity)) {
                        return false;
                    }

                    buff.append("\t").append(f.getFilename());
                }
                
                logger.println(buff.toString());
                return true;    // get here means changelist contains only file(s) to exclude
            }
        }

        return false;
    }
    
    private synchronized int getOrSetMatrixChangeSet(
            @Nonnull AbstractBuild<?,?> build, 
            @Nonnull Depot depot, int newestChange, String projectPath, 
            @Nonnull PrintStream log) 
            throws ParameterSubstitutionException, InterruptedException
    {
        int matrixLastChange = 0;
        // special consideration for matrix builds
        if (build instanceof MatrixRun) {
            log.println("This is a matrix run, trying to use change number from parent/siblings...");
            AbstractBuild<?,?> parentBuild = ((MatrixRun) build).getParentBuild();
            if (parentBuild != null) {
                int parentChange = getLastChange(parentBuild);
                if (parentChange > 0) {
                    // use existing changeset from parent
                    log.println("Latest change from parent is: "+Integer.toString(parentChange));
                    matrixLastChange = parentChange;
                } else {
                    // no changeset on parent, set it for other
                    // matrixruns to use
                    log.println("No change number has been set by parent/siblings. Using latest.");
                    parentBuild.addAction(new P4TagAction(build, depot, newestChange, projectPath, 
                            MacroStringHelper.substituteParameters(getEffectiveP4User(), this, build, null)));
                }
            }
        }
        return matrixLastChange;
    }

    private void flushWorkspaceTo0(Depot depot, Workspace p4workspace, PrintStream log) throws PerforceException {
        saveWorkspaceIfDirty(depot, p4workspace, log);
        depot.getWorkspaces().flushTo("//" + p4workspace.getName() + "/...#0");
    }
    
    @Override
    public boolean processWorkspaceBeforeDeletion(AbstractProject<?, ?> project,
            FilePath workspace, Node node)
                    throws IOException, InterruptedException {
        logger.info("Workspace '"+workspace.getRemote()+"' is being deleted; flushing workspace to revision 0.");
        TaskListener loglistener = new LogTaskListener(java.util.logging.Logger.getLogger(P4SCM.class.getName()), Level.INFO);
        PrintStream log = loglistener.getLogger();
        @SuppressWarnings("deprecation")
        TaskListener listener = new StreamTaskListener(log);
        Launcher launcher = node.createLauncher(listener);
        
        try {
            Depot depot = getDepot(launcher, workspace, project, null, node);
            final String effectiveProjectPath = MacroStringHelper.substituteParameters(
                    projectPath, this, project, node, null);
            Workspace p4workspace = getPerforceWorkspace(
                project,
                effectiveProjectPath,
                depot,
                node,
                null,
                null,
                workspace,
                listener,
                true,
                false);
            flushWorkspaceTo0(depot, p4workspace, log);
        } catch (Exception ex) {
            logger.error(null, ex);
            return false;
        }
        return true;
    }

    @Override
    public boolean requiresWorkspaceForPolling() {
        return false;
    }

    @Override
    public boolean checkout(AbstractBuild<?, ?> build, Launcher launcher,
            FilePath workspace, BuildListener listener, File changelogFile)
            throws IOException, InterruptedException {
        logger.info("P4SCM Checkouting ..., workspace: {}, list its sub files", workspace.getName());
        
        PrintStream log = listener.getLogger();
        
        if(workspace.exists()) {
            CommonUtil.listFilePath(workspace, 1);
        } else {
            log.print(workspace + " does not exist!!!");
        }
        
        changelogFilename = changelogFile.getAbsolutePath();
        log.println(String.format("The change log file name: %s", changelogFilename));
        
        // HACK: Force build env vars to initialize //TODO: not sure why it is called here
        MacroStringHelper.substituteParameters("", this, build, null);
        
        // Use local variables so that substitutions are not saved
        String p4Label = MacroStringHelper.substituteParameters(this.p4Label, this, build, null);
        String viewMask = MacroStringHelper.substituteParameters(this.viewMask, this, build, null);
        Depot depot = getDepot(launcher,workspace, build.getProject(), build, build.getBuiltOn());
        String p4Stream = MacroStringHelper.substituteParameters(this.p4Stream, this, build, null);
        
        // Pull from optional named parameters
        boolean wipeBeforeBuild = overrideWithBooleanParameter(
                "P4CLEANWORKSPACE", build, this.wipeBeforeBuild);
        boolean quickCleanBeforeBuild = overrideWithBooleanParameter(
                "P4QUICKCLEANWORKSPACE", build, this.quickCleanBeforeBuild);
        boolean wipeRepoBeforeBuild = overrideWithBooleanParameter(
                "P4CLEANREPOINWORKSPACE", build, this.wipeRepoBeforeBuild);
        boolean forceSync = overrideWithBooleanParameter(
                "P4FORCESYNC", build, this.forceSync);
        boolean disableChangeLogOnly = overrideWithBooleanParameter(
                "P4DISABLECHANGELOG", build, this.disableChangeLogOnly);
        boolean disableSyncOnly = overrideWithBooleanParameter(
                "P4DISABLESYNCONLY", build, this.disableSyncOnly);
        disableSyncOnly = overrideWithBooleanParameter(
                "P4DISABLESYNC", build, this.disableSyncOnly);
        boolean oneChangelistOnly = overrideWithBooleanParameter(
                "P4ONECHANGELIST", build, false);
        
        // If we're doing a matrix build, we should always force sync.
        if ((Object)build instanceof MatrixBuild || (Object)build instanceof MatrixRun) {
            if (!alwaysForceSync && !wipeBeforeBuild)
                log.println("This is a matrix build; It is HIGHLY recommended that you enable the " +
                            "'Always Force Sync' or 'Clean Workspace' options. " +
                            "Failing to do so will likely result in child builds not being synced properly.");
        }
        
        try {
            
            // keep projectPath local so any modifications for slaves don't get saved TODO: why don't get saved for slaves?
            String effectiveProjectPath= getEffectiveProjectPath(build, 
                    build.getProject(), build.getBuiltOn(), log, depot);
            
            Workspace p4workspace = getPerforceWorkspace(build.getProject(), effectiveProjectPath, depot, build.getBuiltOn(), 
                    build, launcher, workspace, listener, false);
            
            boolean dirtyWorkspace = p4workspace.isDirty();
            saveWorkspaceIfDirty(depot, p4workspace, log);
            
            //Wipe/clean workspace
            String p4config;
            WipeWorkspaceExcludeFilter wipeFilter;
            try {
                p4config = MacroStringHelper.substituteParameters("${P4CONFIG}", this, build, null);
                wipeFilter = new WipeWorkspaceExcludeFilter(".p4config", p4config);
            } catch (ParameterSubstitutionException ex) {
                wipeFilter = new WipeWorkspaceExcludeFilter();
            }
                    
            if (wipeBeforeBuild || quickCleanBeforeBuild) {
                long cleanStartTime = System.currentTimeMillis();
                if (wipeRepoBeforeBuild) {
                    log.println("Clear workspace includes .repository ...");                    
                } else {
                    log.println("Note: .repository directory in workspace (if exists) is skipped during clean.");
                    wipeFilter.exclude(".repository");
                }
                if (wipeBeforeBuild) {
                    log.println("Wiping workspace...");
                    List<FilePath> workspaceDirs = workspace.list(wipeFilter);
                    for (FilePath dir : workspaceDirs) {
                        dir.deleteRecursive();
                    }
                    log.println("Wiped workspace.");
                    forceSync = true;
                }
                if (quickCleanBeforeBuild) {
                    QuickCleaner quickCleaner = new QuickCleaner(depot.getExecutable(), depot.getP4Ticket(), launcher, depot, workspace, wipeFilter);
                    log.println("Quickly cleaning workspace...");
                    quickCleaner.doClean();
                    log.println("Workspace is clean.");
                    if (restoreChangedDeletedFiles) {
                        log.println("Restoring changed and deleted files...");
                        quickCleaner.doRestore();
                        log.println("Files restored.");
                    }
                }
                long cleanEndTime = System.currentTimeMillis();
                long cleanDuration = cleanEndTime - cleanStartTime;

                log.println("Clean complete, took " + cleanDuration + " ms");
            }
            
            // In case of a stream depot, we want Perforce to handle the client views. So let's re-initialize
            // the p4workspace object if it was changed since the last build. Also, populate projectPath with
            // the current view from Perforce. We need it for labeling.
            if (useStreamDepot) {
                if (dirtyWorkspace) {
                    // Support for concurrent builds
                    String p4Client = getConcurrentClientName(workspace, getEffectiveClientName(build, null));
                    p4workspace = depot.getWorkspaces().getWorkspace(p4Client, p4Stream);
                }
                effectiveProjectPath = p4workspace.getTrimmedViewsAsString();
            }
            // If we're not managing the view, populate the projectPath with the current view from perforce
            // This is both for convenience, and so the labeling mechanism can operate correctly
            if (!updateView) {
                effectiveProjectPath = p4workspace.getTrimmedViewsAsString();
            }

            String p4WorkspacePath = "//" + p4workspace.getName() + "/...";
            int lastChange = getLastChange((Run<?,?>)build.getPreviousBuild());
            log.println("Last build changeset: " + lastChange);

            // Determine changeset number
            int newestChange = lastChange;

            List<Changelist> changes;
            if (p4Label != null && !p4Label.trim().isEmpty()) {
                newestChange = depot.getChanges().getHighestLabelChangeNumber(p4workspace, p4Label.trim(), p4WorkspacePath);
            } else {
                if (p4UpstreamProject != null && p4UpstreamProject.length() > 0) {
                    log.println("Using last successful or unstable build of upstream project " + p4UpstreamProject);
                    Job<?,?> job = Jenkins.getInstance().getItemByFullName(p4UpstreamProject, Job.class);
                    if (job == null) {
                        throw new AbortException(
                                "Configured upstream job does not exist anymore: " + p4UpstreamProject + ". Please update your job configuration.");
                    }
                    Run<?,?> upStreamRun = job.getLastSuccessfulBuild();
                    int lastUpStreamChange = CommonUtil.getLastChangeNoFirstChange(upStreamRun);
                    if (lastUpStreamChange > 0) {
                        log.println("Using P4 revision " + lastUpStreamChange + " from upstream project " + p4UpstreamProject);
                        newestChange = lastUpStreamChange;
                    } else {
                        log.println("No P4 revision found in upstream project " + p4UpstreamProject);
                        throw new AbortException(
                                "Configured upstream job has not been run yet: " + p4UpstreamProject + ". Please run it once befor launching a new build.");
                    }
                } else
                if (p4Counter != null && !updateCounterValue) {
                    //use a counter
                    String counterName;
                    counterName = MacroStringHelper.substituteParameters(this.p4Counter, this, build, null);
                    Counter counter = depot.getCounters().getCounter(counterName);
                    newestChange = counter.getValue();
                } else {
                    //use the latest submitted change from workspace, or depot
                    try {
                        List<Integer> workspaceChanges = depot.getChanges().getChangeNumbers(p4WorkspacePath, 0, 1);
                        if (workspaceChanges != null && workspaceChanges.size() > 0) {
                            newestChange = workspaceChanges.get(0);
                        } else {
                            List<Integer> depotChanges = depot.getChanges().getChangeNumbers("//...", 0, 1);
                            if (depotChanges != null && depotChanges.size() > 0) {
                                newestChange = depotChanges.get(0);
                            }
                        }
                    } catch (PerforceException e) {
                        //fall back onto 'change' counter value
                        log.println("Failed to get last submitted changeset in the view, falling back to change counter. Error was: " + e.getMessage());
                        Counter counter = depot.getCounters().getCounter("change");
                        newestChange = counter.getValue();
                    }
                }
            }

            // Set newestChange down to the next available changeset if we're building one change at a time
            if (oneChangelistOnly && build.getPreviousBuild() != null
                    && lastChange > 0 && newestChange > lastChange) {
                List<Integer> workspaceChanges = depot.getChanges().getChangeNumbersInRange(
                        p4workspace, lastChange+1, newestChange, viewMask, showIntegChanges);
                for (int i = workspaceChanges.size()-1; i >= 0; --i) {
                    int changeNumber = workspaceChanges.get(i);
                    Changelist changelist = depot.getChanges().getChangelist(changeNumber, fileLimit);
                    if (!isChangelistExcluded(changelist, build.getProject(), build.getBuiltOn(), p4workspace.getViewsAsString(), log)) {
                        newestChange = changeNumber;
                        break;
                    }
                    log.println("Changelist "+changeNumber+" is composed of file(s) and/or user(s) that are excluded.");
                }
                log.println("Remaining changes: " + workspaceChanges);
                log.println("Building next changeset in sequence: " + newestChange);
            }
            
            if (build instanceof MatrixRun) {
                newestChange = getOrSetMatrixChangeSet(build, depot, newestChange, effectiveProjectPath, log);
            }
            
            if (lastChange <= 0) {
                lastChange = newestChange - MAX_CHANGESETS_ON_FIRST_BUILD;
                if (lastChange < 0) {
                    lastChange = 0;
                }
            }
            
            // Get ChangeLog
            if (!disableChangeLogOnly) {
                logger.debug("Getting change log ...");
                int lastChangeToDisplay = lastChange+1;
                if (lastChange > newestChange) {
                    // If we're building an older change, display it anyway
                    // TODO: This can be considered inconsistent behavior
                    lastChangeToDisplay = newestChange;
                }
                    
                List<Integer> changeNumbersTo;
                if (useViewMaskForChangeLog && useViewMask) {
                    changeNumbersTo = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeToDisplay, newestChange, viewMask, showIntegChanges);
                } else {
                    changeNumbersTo = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeToDisplay, newestChange, showIntegChanges);
                }
                changes = depot.getChanges().getChangelistsFromNumbers(changeNumbersTo, fileLimit);

                if (changes.size() > 0) {
                    // Save the changes we discovered.
                    P4ChangeLogSet.saveToChangeLog(
                            new FileOutputStream(changelogFile), changes);
                    newestChange = changes.get(0).getChangeNumber();
                    // Get and store information about committers
                    P4SCMHelper.retrieveUserInformation(depot, changes);
                } else {
                    // No new changes discovered (though the definition of the workspace or label may have changed).
                    createEmptyChangeLog(changelogFile, (TaskListener)listener, "changelog");
                }
            }

            // Sync workspace
            if (!disableSyncOnly) {
                // Now we can actually do the sync process...
                StringBuilder sbMessage = new StringBuilder("Sync'ing workspace to ");
                StringBuilder sbSyncPath = new StringBuilder(p4WorkspacePath);
                StringBuilder sbSyncPathSuffix = new StringBuilder();
                sbSyncPathSuffix.append("@");

                if (p4Label != null && !p4Label.trim().isEmpty()) {
                    sbMessage.append("label ");
                    sbMessage.append(p4Label);
                    sbSyncPathSuffix.append(p4Label);
                } else {
                    sbMessage.append("changelist ");
                    sbMessage.append(newestChange);
                    sbSyncPathSuffix.append(newestChange);
                }

                sbSyncPath.append(sbSyncPathSuffix);

                if (forceSync || alwaysForceSync)
                    sbMessage.append(" (forcing sync of unchanged files).");
                else
                    sbMessage.append(".");

                log.println(sbMessage.toString());
                String syncPath = sbSyncPath.toString();

                long startTime = System.currentTimeMillis();

                if (useViewMaskForSyncing && useViewMask) {
                    for (String path : viewMask.replaceAll("\r", "").split("\n")) {
                        StringBuilder sbMaskPath = new StringBuilder(path);
                        sbMaskPath.append(sbSyncPathSuffix);
                        String maskPath = sbMaskPath.toString();
                        depot.getWorkspaces().syncTo(maskPath, forceSync || alwaysForceSync, dontUpdateServer);
                    }
                } else {
                    depot.getWorkspaces().syncTo(syncPath, forceSync || alwaysForceSync, dontUpdateServer);
                }
                long endTime = System.currentTimeMillis();
                long duration = endTime - startTime;

                log.println("Sync complete, took " + duration + " ms");
            }

            boolean doSaveProject = false;
            // reset one time use variables...
            if (this.forceSync == true || this.firstChange != -1) {
                this.forceSync = false;
                this.firstChange = -1;
                // save the one-time use variables...
                doSaveProject = true;
            }
            // If we aren't managing the client views, update the current ones
            // with those from perforce, and save them if they have changed.
            if (!this.updateView && !effectiveProjectPath.equals(this.projectPath)) {
                this.projectPath = effectiveProjectPath;
                doSaveProject = true;
            }
            if (doSaveProject) {
                build.getParent().save();
            }

            // Add tagging action that enables the user to create a label
            // for this build.
            build.addAction(new P4TagAction(
                build, depot, newestChange, effectiveProjectPath, MacroStringHelper.substituteParameters(getEffectiveP4User(), this, build, null)));

            build.addAction(new P4SCMRevisionState(newestChange));

            if (p4Counter != null && updateCounterValue) {
                // Set or create a counter to mark this change
                Counter counter = new Counter();
                String counterName = MacroStringHelper.substituteParameters(this.p4Counter, this, build, null);
                counter.setName(counterName);
                counter.setValue(newestChange);
                log.println("Updating counter " + counterName + " to " + newestChange);
                depot.getCounters().saveCounter(counter);
            }

            // remember the p4Ticket if we were issued one
            // otherwise keep the last one issued
            if (depot.getP4Ticket() != null)
                p4Ticket = depot.getP4Ticket();
            
            return true;
        } catch (PerforceException e) {
            log.print("Caught exception communicating with perforce. " + e.getMessage());
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw, true);
            e.printStackTrace(pw);
            pw.flush();
            log.print(sw.toString());
            throw new AbortException(
                    "Unable to communicate with perforce. " + e.getMessage());

        } catch (InterruptedException e) {
            throw new IOException(
                    "Unable to get hostname from slave. " + e.getMessage());
        } catch (NullPointerException e) {
            log.print("Caught exception in perforce-plugin. " + e.getMessage());
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw, true);
            e.printStackTrace(pw);
            pw.flush();
            log.print(sw.toString());
            throw new AbortException(
                    "Caught exception in perfoce-plugin. " + e.getMessage());
        }
    }

    @Override
    public boolean supportsPolling() {
        return true;
    }

    @Override
    public ChangeLogParser createChangeLogParser() {
        
        return new P4ChangeLogParser();
    }
 
    @Override
    public void buildEnvVars(AbstractBuild<?, ?> build,
            Map<String, String> env) {
        super.buildEnvVars(build, env);
        
        // Check nested calls
        int nestedCallsCount = 0;
        for (StackTraceElement ste : (new Throwable()).getStackTrace()) { // Inspect the stacktrace to avoid the infinite recursion
            if (ste.getMethodName().equals("buildEnvVars") && ste.getClassName().equals(P4SCM.class.getName())) {
                if ( ++nestedCallsCount > MAX_BUILD_ENV_VARS_NESTED_CALLS) {
                    return;
                }
            }
        }
             
        try {
            env.put("P4PORT", MacroStringHelper.substituteParameters(p4Port, this, build, env));
            env.put("P4USER", MacroStringHelper.substituteParameters(getEffectiveP4User(), this, build, env));   
            
            // if we want to allow p4 commands in script steps this helps
            if (isExposeP4Passwd()) {
                env.put("P4PASSWD", P4PasswordEncryptor.instance().decryptString(getEffectiveP4Password()));
            }
            // this may help when tickets are used since we are
            // not storing the ticket on the client during login
            if (p4Ticket != null) {
                env.put("P4TICKET", p4Ticket);
            }
            // If we are running concurrent builds, the Jenkins workspace path is different
            // for each concurrent build. Append Perforce workspace name with Jenkins
            // workspace identifier suffix. But, only if we are syncing or allowing Jenkins to 
            // manage workspaces. 
            String effectiveP4Client = getEffectiveClientName(build, env);
            if (!this.disableSyncOnly || this.createWorkspace) {
                effectiveP4Client = getConcurrentClientName(build.getWorkspace(), effectiveP4Client);
            }            
            env.put("P4CLIENT", effectiveP4Client);        
        } catch (ParameterSubstitutionException ex) {
            if (nestedCallsCount < 1) { // substitution failure is acceptable for a nested call
                logger.error("Cannot build environent variables due to unresolved macros", ex);
            }
            //TODO: exit?
        } catch (InterruptedException ex) {
            logger.error("Cannot build environment vars. The method has been interrupted");
        }
        
        P4TagAction pta = build.getAction(P4TagAction.class);
        if (pta != null) {
            if (pta.getChangeNumber() > 0) {
                int lastChange = pta.getChangeNumber();
                env.put("P4_CHANGELIST", Integer.toString(lastChange));
            } else if (pta.isTagged()) {
                String label = pta.getTags().get(0).getName();
                env.put("P4_LABEL", label);
            }
        }

        if (changelogFilename != null) {
            env.put("HUDSON_CHANGELOG_FILE", changelogFilename);
        }
    }
    
    

    @Override
    public SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build,
            Launcher launcher, TaskListener listener)
                    throws IOException, InterruptedException {
      //This shouldn't be getting called, but in case it is, let's calculate the revision anyways.
        P4TagAction action = (P4TagAction)build.getAction(P4TagAction.class);
        if (action==null) {
            //something went wrong...
            return null;
        }
        return new P4SCMRevisionState(action.getChangeNumber());
    }
    
    private SCMRevisionState getCurrentDepotRevisionState(
            @Nonnull Workspace p4workspace, 
            @CheckForNull AbstractProject<?,?> project, 
            @CheckForNull Node node, @Nonnull Depot depot,
        PrintStream logger, int lastChangeNumber) throws IOException, InterruptedException, PerforceException {

        int highestSelectedChangeNumber;
        List<Integer> changeNumbers;

        if (p4Counter != null && !updateCounterValue) {

            // If this is a downstream build that triggers by polling the set counter
            // use the counter as the value for the newest change instead of the workspace view

            Counter counter = depot.getCounters().getCounter(p4Counter);
            highestSelectedChangeNumber = counter.getValue();
            logger.println("Latest submitted change selected by named counter is " + highestSelectedChangeNumber);
            String root = "//" + p4workspace.getName() + "/...";
            changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, highestSelectedChangeNumber, root, false);
        } else {
            // General Case

            // Has any new change been submitted since then (that is selected
            // by this workspace).

            Integer newestChange;
            String effectiveP4Label = MacroStringHelper.substituteParameters(
                    this.p4Label, this, project, node, null);
            if (effectiveP4Label != null && !effectiveP4Label.trim().isEmpty()) {
                //In case where we are using a rolling label.
                String root = "//" + p4workspace.getName() + "/...";
                newestChange = depot.getChanges().getHighestLabelChangeNumber(p4workspace, effectiveP4Label.trim(), root);
            } else {
                Counter counter = depot.getCounters().getCounter("change");
                newestChange = counter.getValue();
            }

            if (useViewMaskForPolling && useViewMask) {
                changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, newestChange, 
                        MacroStringHelper.substituteParameters(viewMask, this, project, node, null), false);
            } else {
                String root = "//" + p4workspace.getName() + "/...";
                changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, newestChange, root, false);
            }
            if (changeNumbers.isEmpty()) {
                // Wierd, this shouldn't be!  I suppose it could happen if the
                // view selects no files (e.g. //depot/non-existent-branch/...).
                // This can also happen when using view masks with polling.
                logger.println("No changes found.");
                return new P4SCMRevisionState(lastChangeNumber);
            } else {
                highestSelectedChangeNumber = changeNumbers.get(0).intValue();
                logger.println("Latest submitted change selected by workspace is " + highestSelectedChangeNumber);
            }
        }

        if (lastChangeNumber >= highestSelectedChangeNumber) {
            // Note, can't determine with currently saved info
            // whether the workspace definition has changed.
            logger.println("Assuming that the workspace definition has not changed.");
            return new P4SCMRevisionState(lastChangeNumber);
        }
        else {
            for (int changeNumber : changeNumbers) {
                if (isChangelistExcluded(depot.getChanges().getChangelist(changeNumber, fileLimit), 
                        project, node, p4workspace.getViewsAsString(), logger)) {
                    logger.println("Changelist "+changeNumber+" is composed of file(s) and/or user(s) that are excluded.");
                } else {
                    return new P4SCMRevisionState(changeNumber);
                }
            }
            return new P4SCMRevisionState(lastChangeNumber);
        }
    }
    
    @CheckForNull
    private Node getPollingNode(@Nonnull AbstractProject<?,?> project) {
        Node buildNode = project.getLastBuiltOn();
        if (pollOnlyOnMaster) {
            buildNode = null;
        } else {
            // try to get an active node that the project is configured to use
            if (!P4SCMHelper.isNodeOnline(buildNode)) {
                buildNode = null;
            }
            if (buildNode == null && !pollOnlyOnMaster) {
                buildNode = P4SCMHelper.getOnlineConfiguredNode(project);
            }
            if (pollOnlyOnMaster) {
                buildNode = null;
            }
        }
        return buildNode;
    }

    @Override
    protected PollingResult compareRemoteRevisionWith(
            AbstractProject<?, ?> project, Launcher launcher,
            FilePath workspace, TaskListener listener,
            SCMRevisionState scmrs)
                    throws IOException, InterruptedException {
        PrintStream logger = listener.getLogger();
        logger.println("Looking for changes...");
        final P4SCMRevisionState baseline;

        if (scmrs instanceof P4SCMRevisionState) {
            baseline = (P4SCMRevisionState)scmrs;
        } else if (project.getLastBuild() != null) {
            baseline = (P4SCMRevisionState)calcRevisionsFromBuild(project.getLastBuild(), launcher, listener);
        } else {
            baseline = new P4SCMRevisionState(-1);
        }

        if (project.getLastBuild() == null || baseline == null) {
            logger.println("No previous builds to use for comparison.");
            return PollingResult.BUILD_NOW;
        }

        try {
            Node buildNode = getPollingNode(project);
            Depot depot;
            if (buildNode == null) {
                depot = getDepot(launcher,workspace,project,null,buildNode);
                logger.println("Using master");
            } else {
                depot = getDepot(buildNode.createLauncher(listener),buildNode.getRootPath(),project,null, buildNode);
                logger.println("Using node: " + buildNode.getDisplayName());
            }

            Workspace p4workspace = getPerforceWorkspace(project, getEffectiveProjectPath(null, project, buildNode, logger, depot), depot, buildNode, null, launcher, workspace, listener, true);
            saveWorkspaceIfDirty(depot, p4workspace, logger);

            int lastChangeNumber = baseline.getRevision();
            SCMRevisionState repositoryState = getCurrentDepotRevisionState(p4workspace, project, buildNode, depot, logger, lastChangeNumber);

            PollingResult.Change change;
            if (repositoryState.equals(baseline)) {
                change = PollingResult.Change.NONE;
            } else {
                change = PollingResult.Change.SIGNIFICANT;
            }

            return new PollingResult(baseline, repositoryState, change);

        } catch (PerforceException e) {
            System.out.println("Problem: " + e.getMessage());
            logger.println("Caught Exception communicating with perforce." + e.getMessage());
            throw new IOException("Unable to communicate with perforce.  Check log file for: " + e.getMessage());
        }
    }

    @Override
    public P4SCMDescriptor getDescriptor() {
        logger.debug("Call P4SCMDescriptor");
        return (P4SCMDescriptor)super.getDescriptor();
    }
    
    /** Get instance of P4SCM Descriptor, TODO: make sure why it return P4SCMDescriptor
     * @return
     */
    public static P4SCMDescriptor getInstance() {
        
        String scmName = P4SCM.class.getSimpleName();
        return (P4SCMDescriptor) Jenkins.getInstance().getScm(scmName);
    }
    
    /**
     * Get the path to p4 executable from a Perforce tool installation.
     *
     * @param tool the p4 tool installation name
     * @return path to p4 tool installation or null
     */
    @CheckForNull
    public P4ToolInstallation getP4Tool(@CheckForNull String tool) {
        List<P4ToolInstallation> installations = getDescriptor().getP4Tools();
        for (P4ToolInstallation i : installations) {
            if (i.getName().equals(tool)) {
                return i;
            }
        }
        return null;
    }
    
    /**
     * Get the path to p4 executable from a Perforce tool installation.
     *
     * @param tool the p4 tool installation name
     * @return path to p4 tool path or an empty string if none is found
     */
    @Nonnull
    public String getP4Executable(@CheckForNull String tool) {
        P4ToolInstallation toolInstallation = getP4Tool(tool);
        if (toolInstallation == null)
            return "p4";
        return toolInstallation.getP4Exe();
    }
    
    @Nonnull
    public String getP4Executable(@CheckForNull String tool, 
            @Nonnull Node node, @Nonnull TaskListener listener) {
        P4ToolInstallation toolInstallation = getP4Tool(tool);
        if (toolInstallation == null)
            return "p4";
        String p4Exe="p4";
        try {
            p4Exe = toolInstallation.forNode(node, listener).getP4Exe();
        } catch (IOException e) {
            listener.getLogger().println(e);
        } catch (InterruptedException e) {
            listener.getLogger().println(e);
        }
        return p4Exe;
    }
    
    /**
     * This only exists because we need to do initialization after we have been brought
     * back to life.  I'm not quite clear on stapler and how all that works.
     * At any rate, it doesn't look like we have an init() method for setting up our Depot
     * after all of the setters have been called.  Someone correct me if I'm wrong...
     *
     * UPDATE: With the addition of PerforceMailResolver, we now have need to share the depot object.  I'm making
     * this protected to enable that.
     *
     * Always create a new Depot to reflect any changes to the machines that
     * P4 actions will be performed on.
     *
     * @param node the value of node
     * @exception ParameterSubstitutionException
     */
    @Nonnull //TODO: why it is different from getDepotRequest
    protected Depot getDepot(@Nonnull Launcher launcher, @Nonnull FilePath workspace,
            @CheckForNull AbstractProject<?,?> project,
            @CheckForNull AbstractBuild<?,?> build, @CheckForNull Node node)
            throws ParameterSubstitutionException, InterruptedException {
        
        P4ExecutorFactory p4Factory = new P4ExecutorFactory(launcher,workspace);

        Depot depot = new Depot(p4Factory);
        
        depot.setClient(MacroStringHelper.substituteParameters(p4Client, this, build, project, node, null));
        depot.setUser(MacroStringHelper.substituteParameters(getEffectiveP4User(), this, build, project, node, null));
        depot.setPort(MacroStringHelper.substituteParameters(p4Port, this, build, project, node, null));
        
        if (build != null) { // We can retrieve all parameters from the build's environment         
            depot.setPassword(getDecryptedP4Passwd(build));
        } else { // project can be null
            depot.setPassword(project != null ? getDecryptedP4Passwd(project, node) : getDecryptedP4Passwd());
        }

        if (p4Ticket != null && !p4Ticket.equals(""))
            depot.setP4Ticket(p4Ticket);
        
        if (node == null)
            depot.setExecutable(getP4Executable(p4Tool));
        else
            depot.setExecutable(getP4Executable(p4Tool,node,TaskListener.NULL));

        // Get systemDrive,systemRoot computer environment variables from
        // the current machine.
        // The current machine is the machine about to do something (run a
        // build, poll the server) according to whatever called getDepot
        String systemDrive = Util.fixEmptyAndTrim(p4SysDrive);
        String systemRoot = Util.fixEmptyAndTrim(p4SysRoot);
        try {
            Computer currentComputer = Computer.currentComputer();
            // A master with no executors seems to throw an NPE here, so
            // we need to check for null.
            if (currentComputer != null) {
                EnvVars envVars = currentComputer.getEnvironment();
                if (systemDrive == null && envVars.containsKey("SystemDrive")) {
                    systemDrive = envVars.get("SystemDrive");
                }
                if (systemRoot == null && envVars.containsKey("SystemRoot")) {
                    systemRoot = envVars.get("SystemRoot");
                }
            }
        } catch (Exception ex) {
            logger.warn(ex.getMessage(), ex);
        }
        depot.setSystemDrive(systemDrive);
        depot.setSystemRoot(systemRoot);

        depot.setCharset(p4Charset);
        depot.setCommandCharset(p4CommandCharset);

        return depot;
    }
    
    /**
     * Gets the P4 user from local or global configs (as a default).
     * @return Effective password. May be null or empty
     * @since 1.3.31
     */
    public @CheckForNull String getEffectiveP4User() {
        return p4User != null ? p4User : getInstance().getP4DefaultUser();
    }
    
    /**
     * Gets the password from local or global configs (as a default).
     * @return Effective password. May be null or empty
     * @since 1.3.31
     */
    public @CheckForNull String getEffectiveP4Password() {
        return p4Passwd != null ? p4Passwd : getInstance().getP4DefaultPassword();
    }
    
    @Extension
    public static final class P4SCMDescriptor extends SCMDescriptor<P4SCM> {
        
        private final static int P4_INFINITE_TIMEOUT_SEC = 0;
        private final static int P4_MINIMAL_TIMEOUT_SEC = 30;
        
        private String p4ClientPattern;
        /**
         * Defines timeout for BufferedReader::readLine() operations (in seconds).
         * Zero value means "infinite";
         */
        private Integer p4ReadlineTimeout;
        
        /**DIsables expose of Perforce password to the build environment*/
        private boolean passwordExposeDisabled;
        
        private @CheckForNull String p4DefaultUser;
        private @CheckForNull String p4DefaultPassword;

        public @CheckForNull String getP4DefaultUser() {
            return p4DefaultUser;
        }

        public @Nonnull String getP4DefaultPassword() {
            return p4DefaultPassword != null ? p4DefaultPassword : "";
        }

        public P4SCMDescriptor() {
            super(P4SCM.class, P4RepositoryBrowser.class); //TODO: use P4RepositoryBrowser
            load();
            logger.debug("Constructed P4SCMDescriptor, load data from persistence");
        }
        
        @Extension
        public static class ItemListenerImpl extends ItemListener {

            @SuppressWarnings("deprecation")
            @Override
            public void onRenamed(Item item, String oldName, String newName) {
                //super.onRenamed(item, oldName, newName);
                for(Project<?, ?>  p: Jenkins.getInstance().getProjects()) {
                    SCM scm = p.getScm();
                    if(scm instanceof P4SCM) {
                        P4SCM p4scm = (P4SCM) scm;
                        if(oldName.equals(p4scm.p4UpstreamProject)) {
                            p4scm.p4UpstreamProject = newName;
                            try {
                                p.save();
                            } catch (IOException e) {
                                logger.warn("Failed to persist project setting during rename from {} to {}", oldName, newName, e);
                            }
                        }
                    }
                }
            }
            
        }
        
        public String getAppName() {
            return Jenkins.getInstance().getDisplayName();
        }
        
        @Override
        public String getDisplayName() {
            return "P4 SCM";
        }
        
        @Override
        public SCM newInstance(StaplerRequest req, JSONObject formData)
                throws FormException {
            logger.debug("New P4SCM instance ...");
            return super.newInstance(req, formData);
        }
        
        /**Generates a random key for p4.config.instanceID*/
        private static final AtomicLong P4_INSTANCE_COUNTER = new AtomicLong();
        public String generateP4InstanceID() {
            // There's no problem even if the counter reaches overflow 
            return Long.toString(P4_INSTANCE_COUNTER.incrementAndGet());
        }
        
        /**
         * List available tool installations.
         *
         * @return list of available p4 tool installations
         */
        public List<P4ToolInstallation> getP4Tools() {
            P4ToolInstallation[] p4ToolInstallations = P4ToolInstallation.getInstance().getInstallations();
            return Arrays.asList(p4ToolInstallations);
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean configure(StaplerRequest req, JSONObject json)
                throws FormException {
            
            CommonUtil.logRequest(req);
            
            logger.debug("Configure Descripter, data: {}", json);
            
            p4ClientPattern = Util.fixEmpty(json.getString("clientPattern"));
            p4DefaultUser = Util.fixEmptyAndTrim(json.getString("defaultUser"));
            setDefaultP4Passwd(Util.fixEmptyAndTrim(json.getString("defaultPassword")));
            passwordExposeDisabled = json.getBoolean("passwordExposeDisabled");
            
            if(Util.fixEmptyAndTrim(json.getString("readLineTimeout")) == null) {
                p4ReadlineTimeout = P4_INFINITE_TIMEOUT_SEC;
            } else {
                try {
                    p4ReadlineTimeout = json.getInt("readLineTimeout");
                } catch (Exception e) {
                    logger.error("Value for readLineTimeout is invalid, will be set to default", e);
                    p4ReadlineTimeout = P4_INFINITE_TIMEOUT_SEC;
                } 
            }

            
            save();
            return super.configure(req, json);
        }
        
        /**
         * Gets client workspace name pattern
         */
        public String getP4ClientPattern() {
            if (p4ClientPattern == null) {
                return ".*";
            } else {
                return p4ClientPattern;
            }
        }

        public int getP4ReadLineTimeout() {
            if (p4ReadlineTimeout == null) {
                return P4_INFINITE_TIMEOUT_SEC;
            } else {
                return p4ReadlineTimeout;
            }
        }

        public boolean hasP4ReadlineTimeout() {  
            return getP4ReadLineTimeout() != P4_INFINITE_TIMEOUT_SEC;
        }
        
        public String getP4ReadLineTimeoutStr() {
            return hasP4ReadlineTimeout() ? p4ReadlineTimeout.toString() : "";
        }
        
        public boolean isPasswordExposeDisabled() {
            return passwordExposeDisabled;
        }
        
        private void setDefaultP4Passwd(@CheckForNull String passwd) {
            if (passwd == null) {
                p4DefaultPassword = null;
                return;
            }
            
            P4PasswordEncryptor encryptor = P4PasswordEncryptor.instance();
            if (encryptor.appearsToBeAnEncryptedPassword(passwd)) {
                p4DefaultPassword = passwd;
            } else {
                p4DefaultPassword = encryptor.encryptString(passwd);
            }
        }
        
        public List<String> getAllLineEndChoices() {
            return Arrays.asList(
                "local",
                "unix",
                "mac",
                "win",
                "share"
            );
        }
        
        public FormValidation doValidateNamePattern(StaplerRequest req) {          
            logger.debug("Validating name pattern ...");
            String namePattern = Util.fixEmptyAndTrim(req.getParameter("value"));
            if (namePattern != null) {
                try {
                    Pattern.compile(namePattern);
                } catch (PatternSyntaxException exception) {
                    return FormValidation.error("Pattern format error:\n"+exception.getMessage());
                }
            }
            return FormValidation.ok();
        }
        
        public FormValidation doValidateP4ReadLineTimeout(StaplerRequest req) {
            logger.debug("Validating p4 readline timeout ...");
            String valueStr = Util.fixEmptyAndTrim(req.getParameter("value"));
              if (valueStr != null) {
                try {
                    int val = Integer.parseInt(valueStr);
                    if (val < P4_MINIMAL_TIMEOUT_SEC) {
                        return FormValidation.error("P4 ReadLine timeout should exceed "+P4_MINIMAL_TIMEOUT_SEC+" seconds. Value will be ignored");
                    }
                } catch (NumberFormatException ex) {
                    return FormValidation.error("Number format error: "+ex.getMessage());
                }
            }
            return FormValidation.ok();
        }
        
        public FormValidation doValidatePerforceUsername(StaplerRequest req) {
            logger.debug("Validating perforce username ...");
            String username = Util.fixEmptyAndTrim(req.getParameter("user"));
            if (username == null) {
                return FormValidation.warning("No user specified. A default user '"+getInstance().getP4DefaultUser()+"' will be used");
            }
            logger.debug("Validated username: {}", username);
            return FormValidation.ok();
        }
        
        /**
         * Checks if the perforce login credentials are good.
         */
        public FormValidation doValidatePerforceLogin(StaplerRequest req) {
            logger.debug("Validating perforce login ...");
            CommonUtil.logRequest(req);
            try {
            Depot depot = getDepotFromRequest(req);
                if (depot != null) {
                    depot.getStatus().isValid();
                } else {
                    return FormValidation.warning("No depot found." + Util.fixEmptyAndTrim(errMsg) == null? "" : errMsg);
                }
            } catch (PerforceException e) {
                return FormValidation.error(e.getMessage());
            }
            return FormValidation.ok();
        }
        
        public @Nonnull String getDecryptedP4DefaultPassword() {
            P4PasswordEncryptor encryptor = P4PasswordEncryptor.instance();
            return encryptor.decryptString(p4DefaultPassword);
        }
        
        protected String errMsg = null;
        
        protected Depot getDepotFromRequest(StaplerRequest request) throws PerforceException {
            
            errMsg = null;
            
            String port = fixNull(request.getParameter("port")).trim();
            String tool = fixNull(request.getParameter("tool")).trim();
            
            // Credentials
            String user = fixNull(request.getParameter("user")).trim();
            String pass = fixNull(request.getParameter("pass")).trim();
            if (user.isEmpty()) {
                user = getP4DefaultUser();
                pass = getDecryptedP4DefaultPassword();
            }
            
            if (port.length() == 0) { // Not enough entered yet
                errMsg = "Port is not specified";
                return null;
            }
            /*
            if(tool.length() == 0) {
                errMsg = "Tool is not specified";
                return null;
            }*/
            
            logger.debug("Getting depot with Tool:{}, Port:{}, user:{}, pass:{}", tool, port, user, pass);
            
            Depot depot = new Depot();
            depot.setUser(user);
            P4PasswordEncryptor encryptor = P4PasswordEncryptor.instance();
            if (encryptor.appearsToBeAnEncryptedPassword(pass)) {
                depot.setPassword(encryptor.decryptString(pass));
            } else {
                depot.setPassword(pass);
            }
            depot.setPort(port);

            String exe = "p4";
            
            if(tool.length() != 0) {
                P4ToolInstallation[] installations = P4ToolInstallation.getInstance().getInstallations();
                for (P4ToolInstallation i : installations) {
                    if (i.getName().equals(tool)) {
                        exe = i.getP4Exe();
                    }
                }
            }
            depot.setExecutable(exe);

            try {
                Counter counter = depot.getCounters().getCounter("change");
                if (counter != null)
                    return depot;
            } catch (PerforceException e) {
                logger.error("Fail to get counters when getting depot", e);
                throw e;
            }

            return null;
        }
        
        /**
         * Checks to see if the specified workspace is valid.
         * The method also checks forbidden variables in the client name.
         * (see <a href="https://wiki.jenkins-ci.org/display/JENKINS/Perforce+Plugin">
         * Perforce Plugin Wiki page</a>) 
         * to get the clarification of forbidden variables.
         * An improper usage of the variable may corrupt Perforce workspaces in project builds. 
         */
        public FormValidation doValidateP4Client(StaplerRequest req) {
            String workspace = Util.fixEmptyAndTrim(req.getParameter("client"));          
            if (workspace == null) {
                return FormValidation.error("You must enter a workspaces name");
            }
            
            logger.debug("Validating workspace: {} ...", workspace);
            try {
                // Check P4 client pattern first, because workspace check fails on valid client names with variables
                if (!workspace.matches(getP4ClientPattern())) {
                    return FormValidation.error("Client name doesn't meet global pattern: "+getP4ClientPattern());
                }
                
                // Check forbidden variables
                for (String variableName : P4CLIENT_FORBIDDEN_VARIABLES) {
                    if (MacroStringHelper.containsVariable(workspace, variableName)) {
                        return FormValidation.error(Messages.P4SCM_doValidateP4Client_forbiddenVariableError(variableName));
                    }
                }
                
                // Then, check depot 
                Depot depot = getDepotFromRequest(req);
                if (depot == null) {
                    return FormValidation.error(
                            "Unable to check workspace against depot");
                }
                
                // Then, check workspace
                Workspace p4Workspace =
                    depot.getWorkspaces().getWorkspace(workspace, "");
                if (p4Workspace.getAccess() == null ||
                        p4Workspace.getAccess().equals(""))
                    return FormValidation.warning("Workspace does not exist. " +
                            "If \"Let Hudson/Jenkins Manage Workspace View\" is check" +
                            " the workspace will be automatically created.");
                String owner = p4Workspace.getOwner();
                if( owner != null && !owner.equals(depot.getUser()) ) {
                    return FormValidation.warning(Messages.P4SCM_doValidateP4Client_workspaceOwnerWarning(workspace, depot.getUser(), owner));
                }
            } catch (PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking workspace");
            }

            return FormValidation.ok();
        }
        
        /**
         * Checks if the specified stream is valid.
         */
        public FormValidation doValidateStream(StaplerRequest req) {
            
            String stream = Util.fixEmptyAndTrim(req.getParameter("stream"));
            if (stream == null) {
                return FormValidation.error("You must enter a stream");
            }
            
            if (!stream.endsWith("/...")) {
                stream += "/...";
            }

            if (!CommonUtil.isValidSpec(stream)) {
                return FormValidation.error("Invalid depot path:" + stream);
            }
            
            logger.debug("Validating stream: {} ...", stream);
            try {
                
                Depot depot = getDepotFromRequest(req);
                if (depot == null) {
                    return FormValidation.error(
                            "Unable to check stream against depot");
                }

                String workspace = Util.fixEmptyAndTrim(req.getParameter("client"));
                
                if (!depot.getStatus().exists(stream)) {
                    return FormValidation.error("Stream does not exist");
                }

                Workspace p4Workspace = depot.getWorkspaces().getWorkspace(workspace, "");
                // Warn if workspace exists and is not associated with a stream
                if (p4Workspace.getAccess() != null && !p4Workspace.getAccess().equals("") &&
                        (p4Workspace.getStream() == null || p4Workspace.getStream().equals(""))) {
                    return FormValidation.warning("Workspace '" + workspace + "' already exists and is not associated with a stream. " +
                        "If Jenkins is allowed to manage the workspace view, this workspace will be switched to a stream workspace.");
                }
            } catch (PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking stream: " + e.getLocalizedMessage());
            }

            return FormValidation.ok();
        }
        
        /**
         * Checks to see if the specified ClientSpec is valid.
         */
        public FormValidation doValidateClientSpec(StaplerRequest req) {

            String clientspec = Util.fixEmptyAndTrim(req.getParameter("clientSpec"));
            if (clientspec == null) {
                return FormValidation.error("You must enter a path to a ClientSpec file");
            }

            if (!CommonUtil.isValidClientSpec(clientspec)) {
                return FormValidation.error("Invalid depot path:" + clientspec);
            }

            String workspace = Util.fixEmptyAndTrim(req.getParameter("client"));
            try {
                
                Depot depot = getDepotFromRequest(req);
                if (depot == null) {
                    return FormValidation.error(
                            "Unable to check ClientSpec against depot");
                }
                
                if (!depot.getStatus().exists(clientspec)) {
                    return FormValidation.error("ClientSpec does not exist");
                }

                Workspace p4Workspace = depot.getWorkspaces().getWorkspace(workspace, "");
                // Warn if workspace exists and is associated with a stream
                if (p4Workspace.getAccess() != null && !p4Workspace.getAccess().equals("") &&
                        p4Workspace.getStream() != null && !p4Workspace.getStream().equals("")) {
                    return FormValidation.warning("Workspace '" + workspace + "' already exists and is associated with a stream. " +
                        "If Jenkins is allowed to manage the workspace view, this workspace will be switched to a local workspace.");
                }
            } catch (PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking ClientSpec: " + e.getLocalizedMessage());
            }

            return FormValidation.ok();
        }
        
        /**
         * Checks if the change list entered exists
         */
        public FormValidation doCheckChangeList(StaplerRequest req) {

            String change = fixNull(req.getParameter("change")).trim();
            if (change.length() == 0) { // nothing entered yet
                return FormValidation.ok();
            }
            
            try {
                Depot depot = getDepotFromRequest(req);
                
                if (depot == null) {
                    return FormValidation.error(
                            "Unable to check ChangeList against depot");
                }
                
                try {
                    int number = Integer.parseInt(change);
                    Changelist changelist = depot.getChanges().getChangelist(number, -1);
                    if (changelist.getChangeNumber() != number)
                        throw new PerforceException("broken");
                } catch (Exception e) {
                    return FormValidation.error("Changelist: " + change + " does not exist.");
                }
            } catch(PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking ChangeList: " + e.getLocalizedMessage());
            }

            return FormValidation.ok();
        }
        
        /**
         * Performs syntactical check on the P4Label
         */
        public FormValidation doValidateP4Label(StaplerRequest req, @QueryParameter String label) {
            label = Util.fixEmptyAndTrim(label);
            if (label == null)
                return FormValidation.ok();

            try {
                Depot depot = getDepotFromRequest(req);
                
                if(depot == null) {
                    return FormValidation.error(
                            "Unable to check ChangeList against depot");
                }
                
                Label p4Label = depot.getLabels().getLabel(label);
                if (p4Label.getAccess() == null || p4Label.getAccess().equals(""))
                    return FormValidation.error("Label does not exist");
            } catch (PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking label");
            }
            
            return FormValidation.ok();
        }

        /**
         * Performs syntactical and permissions check on the P4Counter
         */
        public FormValidation doValidateP4Counter(StaplerRequest req, @QueryParameter String counter) {
            counter= Util.fixEmptyAndTrim(counter);
            if (counter == null)
                return FormValidation.ok();
            
            try {
                
                Depot depot = getDepotFromRequest(req);
                
                if(depot == null) {
                    return FormValidation.error(
                            "Unable to check ChangeList against depot");
                }
                
                Counters counters = depot.getCounters();
                Counter p4Counter = counters.getCounter(counter);
                // try setting the counter back to the same value to verify permissions
                counters.saveCounter(p4Counter);
            } catch (PerforceException e) {
                return FormValidation.error(
                        "Error accessing perforce while checking counter: " + e.getLocalizedMessage());
            }
                
            return FormValidation.ok();
        }
        
        /**
         * Checks to see if the specified project exists and has p4 info.
         */
        public FormValidation doValidateP4UpstreamProject(StaplerRequest req, @QueryParameter String project) {
            project = Util.fixEmptyAndTrim(project);
            if (project == null) {
                // well, it is not really OK, but it means it will not be used, so no error
                return FormValidation.ok();
            }

            Job<?,?> job = Jenkins.getInstance().getItemByFullName(project, Job.class);
            if (job == null) {
                return FormValidation.error(hudson.tasks.Messages.BuildTrigger_NoSuchProject(project, AbstractProject.findNearest(project).getName()));
            }

            Run<?,?> upStreamRun = job.getLastSuccessfulBuild();
            int lastUpStreamChange = CommonUtil.getLastChangeNoFirstChange(upStreamRun);
            if (lastUpStreamChange < 1) {
                FormValidation.warning("No Perforce change found in this project");
            }

            return FormValidation.ok();
        }
        
        public FormValidation doValidateForceSync(StaplerRequest req) {
            Boolean forceSync = Boolean.valueOf(fixNull(req.getParameter("forceSync")).trim());
            Boolean alwaysForceSync = Boolean.valueOf(fixNull(req.getParameter("alwaysForceSync")).trim());
            Boolean dontUpdateServer = Boolean.valueOf(fixNull(req.getParameter("dontUpdateServer")).trim());

            if ((forceSync || alwaysForceSync) && dontUpdateServer) {
                return FormValidation.error("Don't Update Server Database (-p) option is incompatible with force syncing! Either disable -p, or disable force syncing.");
            }
            return FormValidation.ok();
        }
        
        /**
         * Checks if the value is a valid user name/regex pattern.
         */
        public FormValidation doValidateExcludedUsers(StaplerRequest req) {
            String excludedUsers = fixNull(req.getParameter("excludedUsers")).trim();
            List<String> users = Arrays.asList(excludedUsers.split("\n"));

            for (String regex : users) {
                regex = regex.trim();
                if (regex.equals("")) continue;

                try {
                    regex = regex.replaceAll("\\$\\{[^\\}]*\\}","SOMEVARIABLE");
                    Pattern.compile(regex);
                }
                catch (PatternSyntaxException pse) {
                    return FormValidation.error("Invalid regular express ["+regex+"]: " + pse.getMessage());
                }
            }
            return FormValidation.ok();
        }

        /**
         * Checks if the value is a valid file path/regex file pattern.
         */
        public FormValidation doValidateExcludedFiles(StaplerRequest req) {
            String excludedFiles = fixNull(req.getParameter("excludedFiles")).trim();
            Boolean excludedFilesCaseSensitivity = Boolean.valueOf(fixNull(req.getParameter("excludedFilesCaseSensitivity")).trim());
            List<String> files = Arrays.asList(excludedFiles.split("\n"));
            for (String file : files) {
                // splitting with \n can still leave \r on some OS/browsers
                // trimming should eliminate it.
                file = file.trim();
                // empty line? lets ignore it.
                if (file.equals("")) continue;
                // check to make sure it's a valid file spec
                if (!CommonUtil.isValidFileSpec(file) ) {
                    return FormValidation.error("Invalid file spec ["+file+"]: Not a perforce file spec.");
                }
                // check to make sure the globbing regex will work
                // (ie, in case there are special characters that the user hasn't escaped properly)
                try {
                    file = file.replaceAll("\\$\\{[^\\}]*\\}","SOMEVARIABLE");
                    CommonUtil.doesFilenameMatchP4Pattern("somefile", file, excludedFilesCaseSensitivity);
                }
                catch (PatternSyntaxException pse) {
                    return FormValidation.error("Invalid file spec ["+file+"]: " + pse.getMessage());
                }
            }
            return FormValidation.ok();
        }
        
        public FormValidation doCheckViewMask(StaplerRequest req) {
            String view = Util.fixEmptyAndTrim(req.getParameter("viewMask"));
            if (view != null) {
                for (String path : view.replace("\r","").split("\n")) {
                    if (path.startsWith("-") || path.startsWith("\"-"))
                        return FormValidation.error("'-' not yet supported in view mask:" + path);
                    if (!CommonUtil.isValidSpec(path))
                        return FormValidation.error("Invalid depot path:" + path);
                }
            }
            return FormValidation.ok();
        }
    } 

}
